/**
 *
 * Copyright 2009 Marco Speranza <marco.speranza79@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.commons.database.jconnectionpool;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.sql.ConnectionEvent;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

/**
 * Classe che gestisce il pool di connessioni al DB
 * 
 * @author <a href="mailto:msperanza@users.sourceforge.net">Marco Speranza</a>
 * @version $Id: DataBaseManager.java,v 1.1 2006/08/10 09:48:09 msperanza Exp $
 */
public class PoolConnectionManager {

    private static Logger log = Logger.getLogger(PoolConnectionManager.class);
    private final ConnectionPoolDataSource connectionPoolDS;

    private List connPooList;

    protected List idleConnection;
    private IdlePooledConnectionTh idlePooledConnectionTh;

    volatile private int initialCapacity = 5;
    volatile private int requestedConnection;

    private Boolean closeInProgress;

    /**
     * Costruttore
     * 
     * @param connectionPoolDataSource
     *            Instanza del poolConnection data Source.
     * 
     */
    public PoolConnectionManager(
	    ConnectionPoolDataSource connectionPoolDataSource) {
	connectionPoolDS = connectionPoolDataSource;
    }

    /**
     * Rilascia una connessione al DB e la rende disponibile
     * 
     * @param conn
     *            Connessione da rilasciare
     * @throws SQLException
     *             Errore nella chiusura della connessione al DB
     */
    final public synchronized void closeConnection(final Connection conn)
	    throws SQLException {
	if (conn != null) {
	    log.debug("release connection #" + requestedConnection--);
	    conn.close();
	}
    }

    /**
     * Chiude il pool di connessioni
     * 
     * @throws SQLException
     *             Errore nella chiusura del pool di Connessioni
     */
    final public void closePool() throws SQLException {
	int i = 0;

	synchronized (closeInProgress) {
	    closeInProgress = Boolean.TRUE;
	}

	synchronized (connPooList) {
	    log.debug(" ...Closing " + connPooList.size()
		    + " connection polls...");
	    Iterator it = connPooList.iterator();
	    while (it.hasNext()) {
		try {
		    WrapPooledConnection element = (WrapPooledConnection) it
			    .next();
		    element.getPooledConnection().close();
		    i++;
		} catch (SQLException e) {
		    // ignore
		    log.error(e.getMessage(), e);
		}
	    }
	    connPooList.clear();
	}
	synchronized (idleConnection) {
	    log.debug(" ...Closing " + idleConnection.size()
		    + " idle connection polls...");
	    Iterator it = idleConnection.iterator();
	    while (it.hasNext()) {
		try {
		    WrapPooledConnection element = (WrapPooledConnection) it
			    .next();
		    element.getPooledConnection().close();
		    i++;
		} catch (SQLException e) {
		    // ignore
		    log.error(e.getMessage(), e);
		}
	    }
	    idleConnection.clear();
	}

	idlePooledConnectionTh.stop();

	synchronized (closeInProgress) {
	    closeInProgress = Boolean.FALSE;
	}

	log.debug(" ...done. Closed " + i + "connection polls.");

    }

    /**
     * Creazione del pool di connessione
     * 
     * @param url
     *            Stringa di connessione al DB
     * @param user
     *            Userid utilizzata per connettersi al DB
     * @param pwd
     *            Password necessaria per la connessione al DB
     */
    public void createPool() throws SQLException {
	init();
    }

    /**
     * Restituisce un nuova connessione al DB
     * 
     * @return Nuova connessione al DB
     * @throws SQLException
     *             Errore nella creazione della nuova connessione
     */
    final public synchronized Connection getConnection() throws SQLException {

	log.debug("request new connection #" + ++requestedConnection);
	Connection conn = null;
	PooledConnection connPool = getAvaiableConnection();
	if (connPool == null) {
	    log.debug("   creating new poolled connection...");
	    connPool = connectionPoolDS.getPooledConnection();
	    WrapPooledConnection wrap = new WrapPooledConnection(connPool, this);
	    wrap.setAvaiable(false);
	    connPool.addConnectionEventListener(wrap);
	    addToLast(wrap);
	    log.debug("   ... created.");
	}

	conn = connPool.getConnection();
	conn.setAutoCommit(false);
	return conn;

    }

    public void setInitialPoolCapacity(final int initialCapacity) {
	this.initialCapacity = initialCapacity;
    }

    protected synchronized void fairedConnectionAvaiable(
	    final WrapPooledConnection connPool) throws SQLException {
	if (getPoolSize() > initialCapacity) {
	    removePoolItem(connPool);
	    idleConnection.add(connPool);
	    /*
	     * removePoolItem(connPool); connPool.getPooledConnection().close();
	     */
	} else {
	    moveToFirst(connPool);
	}

    }

    protected synchronized void fairedConnectionErrorOccurred(
	    final WrapPooledConnection connPool, final ConnectionEvent event) {
	synchronized (closeInProgress) {
	    if (closeInProgress.booleanValue()) {
		return;
	    }
	}
	removePoolItem(connPool);
	if (log.isEnabledFor(Level.ERROR) && event != null
		&& event.getSQLException() != null)
	    log.error(event.getSQLException().getMessage(), event
		    .getSQLException());

    }

    protected synchronized int getPoolSize() {
	return connPooList.size();
    }

    private boolean removePoolItem(final Object obj) {
	log.debug(" ...Remove from active pool..");
	return connPooList.remove(obj);
    }

    private void addToLast(final Object obj) {
	connPooList.add(obj);
    }

    /**
     * 
     * @return PooledConnection torna una pooled connection disponibile oppure
     *         null
     */
    private PooledConnection getAvaiableConnection() {
	WrapPooledConnection pConn = null;
	if (connPooList.size() == 0) {
	    return null;
	}
	pConn = (WrapPooledConnection) connPooList.get(0);
	if (pConn == null) {
	    return null;
	}
	if (pConn.isAvaiable()) {
	    // E' una connessione disponibile contenuta nella lista iniziale di
	    // connessioni
	    log.debug("     return avaialbe from init pool connection ");
	    pConn.setAvaiable(false);
	    moveToLast(pConn);

	    return pConn.getPooledConnection();
	} else {

	    if (idleConnection.size() != 0) {
		log.debug("     return avaialbe from idle pool connection ");
		pConn = (WrapPooledConnection) idleConnection.remove(0);
		pConn.setAvaiable(false);
		connPooList.add(pConn);
		return pConn.getPooledConnection();
	    }

	}
	return null;
    }

    public boolean isDatabaseOn() {
	try {
	    PooledConnection connPool = connectionPoolDS.getPooledConnection();
	    Connection c = connPool.getConnection();
	    if (c != null) {
		c.close();
	    } else {
		connPool.close();
	    }
	    return true;
	} catch (SQLException e) {
	    return false;
	}
    }

    private void init() throws SQLException {
	closeInProgress = Boolean.FALSE;
	connPooList = Collections.synchronizedList(new ArrayList(
		initialCapacity));

	idleConnection = Collections.synchronizedList(new ArrayList(0));

	log.debug("init pool connection...");
	for (int i = 0; i < initialCapacity; i++) {
	    try {
		PooledConnection connPool = connectionPoolDS
			.getPooledConnection();
		WrapPooledConnection wrap = new WrapPooledConnection(connPool,
			this);
		connPool.addConnectionEventListener(wrap);
		connPooList.add(wrap);
	    } catch (SQLException e) {
		log.error("init pool failed! :" + e.getMessage(), e);
		throw e;
	    }
	}

	idlePooledConnectionTh = new IdlePooledConnectionTh(this);

	log.debug("...created " + connPooList.size() + " poolledConnection of "
		+ initialCapacity);
    }

    private void moveToFirst(final Object obj) {
	synchronized (connPooList) {
	    Object w = connPooList.remove(connPooList.indexOf(obj));
	    connPooList.add(0, w);
	}
    }

    private void moveToLast(final Object obj) {
	synchronized (connPooList) {
	    Object w = connPooList.remove(connPooList.indexOf(obj));
	    connPooList.add(w);
	}
    }

}
